package org.knot.mockejb3;

import java.io.Serializable;
import java.lang.reflect.*;
import java.rmi.RemoteException;
import javax.ejb.*;

import org.knot.mockejb3.exception.MethodNotImplementedException;
import org.knot.mockejb3.interceptor.*;
import org.knot.mockejb3.transcation.TransactionManager;
import org.knot.mockejb3.transcation.TransactionPolicy;



/**
 * Serves as a proxy for all calls to the bean. 
 * Provided implementation of  
 * <code>javax.ejb.EJBObject</code> and <code>javax.ejb.EJBLocalObject</code> interfaces.
 * Provides API for working with interceptors. 
 * 
 * @author Alexander Ananiev
 */
public class MockEjbObject  implements InvocationHandler, Serializable {

    private InterceptorInvoker interceptorInvoker= new InterceptorInvoker();
    
    private Class ifaceClass;
    
    private Object bean;
    
    private Object proxy;
    
    private static MethodContainer standardMethods;
    
    
    /**
     * Standard methods required by EJBObject and EJBLocalObject implemented by
     * this class. 
     */
    static {
        standardMethods = new MethodContainer( MockEjbObject.class );
        standardMethods.add("getHandle");
        standardMethods.add("getPrimaryKey");
        standardMethods.add("remove");

        Class objectArg[] = {Object.class};
        standardMethods.add("isIdentical", objectArg );
        
        // equals is a convenience method
        standardMethods.add("equals", objectArg );
        standardMethods.add("hashCode" );
        // another convenience method
        standardMethods.add("toString" );
        // EJBBeanAccess
        standardMethods.add("getBean");
        standardMethods.add("getEjbContext");
        
    }
    
    
    MockEjbObject( Class ifaceClass ) {
        this.ifaceClass = ifaceClass;
    }


    /**
     * Adds the interceptor to the interceptor list for this bean.
     * @param interceptor interceptor to add
     * @deprecated Use AspectSystem and poincuts to add interceptors
     */
    public void addInterceptor( Interceptor interceptor ){
        
        AspectSystemFactory.getAspectSystem().add( 
            new ClassPointcut( ifaceClass, false), interceptor);       
    }
    
    
    /**
     * Sets the transaction policy for the {@link TransactionManager} 
     * which is always part of the interceptor list. 
     * <br>The default policy is "Supports".
     * @param policy transaction policy as defined by {@link TransactionPolicy} 
     * enumeration. 
     * 
     * @deprecated use TransactionManager with the AspectSystem to set transaction policies
     */
    public void setTransactionPolicy( TransactionPolicy policy ){

        interceptorInvoker.setContext( TransactionManager.POLICY_CONTEXT_KEY, policy );
        
    }

    
    /** Creates a new instance */
    Object createProxy( Object bean) {

        this.bean = bean;
        proxy = Proxy.newProxyInstance( ifaceClass.getClassLoader(), 
            new Class[] { ifaceClass, EjbBeanAccess.class }, this  );
        return proxy;
        
    }
    
    /** Creates a new instance */
    Object createProxy( Object bean, MockEjbContext ejbContext ) {

        this.bean = bean;

        proxy = Proxy.newProxyInstance( ifaceClass.getClassLoader(), 
            new Class[] { ifaceClass, EjbBeanAccess.class }, this  );
        
        // Now we can provide proxy to the context 
        ejbContext.setEjbObjectProxy( proxy );
        interceptorInvoker.setContext( MockEjbContext.class.getName(), ejbContext );
        
        return proxy;
        
    }
    
    /**
     * Invokes the target bean's method by delegating to the <code>InvocationContext</code>
     * which calls interceptors and then the bean itself.
     * If we're dealing with the standard EJB method, this object provides the implementation
     * of these methods instead of the target bean. All interceptors are still invoked as before. 
     */
    public Object invoke( Object proxy, Method ifaceMethod, Object[] paramVals ) 
        throws Throwable {
        
        Method beanMethod = null;
        Object returnValue = null;
        
        Object beanToInvoke = null;
        
        beanMethod = standardMethods.find( ifaceMethod );
        if ( beanMethod != null ) {
            beanToInvoke= this;
        }
        else {
            beanToInvoke= this.bean;
            
            beanMethod = bean.getClass().getMethod( ifaceMethod.getName(), 
                ifaceMethod.getParameterTypes() );
        }
        
        returnValue = interceptorInvoker.invoke( proxy, ifaceMethod, beanToInvoke,  beanMethod, paramVals );
               
        return returnValue;
    }
    
    
    /**
     * Returns <code>MockEjbContext</code> object for the bean backed by this 
     * MockEjbObject. <code>MockEjbContext</code> implements SessionContext and MesageDrivenContext.
     * Additionally it provides some convenience methods. 
     * @return MockEjbContext instance
     */
    public MockEjbContext getEjbContext(){
        
        if ( interceptorInvoker.getContext( MockEjbContext.class.getName())== null )
            throw new IllegalStateException( "Context does not exist. Most likely this EJB has not been created yet");
        
        return (MockEjbContext) interceptorInvoker.getContext( MockEjbContext.class.getName());
    }

    // Implementation of the standard methods from Remote and Local ifaces


    /**
     * This method is not supported. 
     */
    public Handle getHandle() throws RemoteException {
        throwMethodNotImplemented( "getHandle");
        return null;
    }

    public Object getPrimaryKey() {
        return getEjbContext().getPrimaryKey();
        
    }

    /**
     * Currently this method does not do anything
     * TODO: should call ejbRemove for stateful session bean
     * @see javax.ejb.EJBObject#remove()
     */
    public void remove() throws RemoveException {
        
    }

    /**
     * Test if a given object is identical to the invoked object.
     * Works for both EJBObject and EJBLocalObject interfaces. 
     * It uses address equality so the provided parameter does not
     * have to be EJB-specific.
     * @param object an object to test for identity with the invoked object. 
     * @return true if the given object is identical to this object
     */
    public boolean isIdentical(Object object) {
        return equals(object);
    }

    /**
     * Tests if this object is equals to the given object.
     * If the given object is a proxy to another bean, the pointer equality 
     * between proxies is used.
     * Otherwise, <code>super.equals()</code> is called.
     * In other words, this method can be used to compare MockEjbObject instances 
     * as well as bean dynamic proxies returned by Home <code>create()</code>.
     * 
     * @param obj object to compare with
     * @return <code>true</code> if this object or the dynamic proxy it holds equals
     * to the given object. 
     * 
     */
    public boolean equals( Object obj ){
        // if the object is proxy
        if (ifaceClass.isInstance( obj ) )
            return (proxy==obj);
        else
            return super.equals( obj );
    }
    
    public int hashCode() {
        return ifaceClass.hashCode();
    }
    
    // EjbBeanAccess
    public Object getBean(){
    	return bean;
    }
    

    /**
     * Provides string representation of this <code>MockEjbObject</code> and 
     * the its bean implementation object.
     * @see java.lang.Object#toString()
     */
    public String toString(){
        return "EJB object: " +
            " BusinessIface: "+ifaceClass.getName() +"\nBean: "+bean.getClass().getName();
    }

    /**
     * Helper method to throw NotImplementedException for this class
     * @param methodName
     */
    private void throwMethodNotImplemented( String methodName ){

        throw new MethodNotImplementedException( methodName, 
            this.getClass().getName() );

    }

}
